<?php

namespace yunj\library\lock;

use yunj\library\traits\Redis;

class Lock {

    use Redis;

    /**
     * 是否可以执行lua脚本
     * @var bool
     */
    private $lua = true;

    /**
     * @var string
     */
    private $key;

    /**
     * @var int
     */
    private $ttl;

    /**
     * 锁获取的等待时间
     * @var int
     */
    private $waitTime;

    /**
     * 锁获取时间
     * @var int
     */
    private $getTime;

    /**
     * @param string $key
     * @param int $ttl 锁过期时间
     * @param int $waitTime 等待时间（要求大于过期时间）
     */
    public function __construct(string $key, int $ttl = 10, int $waitTime = 60) {
        $this->key = $key;
        $this->ttl = $ttl;
        $this->waitTime = $waitTime;
    }

    /**
     * @param bool $lua
     * @return Lock
     */
    public function setLua(bool $lua = true): Lock {
        $this->lua = $lua;
        return $this;
    }

    /**
     * 锁获取
     * @return Lock
     * @throws \ErrorException
     */
    public function get(): Lock {
        $beginTime = time();
        while (true) {
            if (time() > ($beginTime + $this->waitTime)) {
                throw new \ErrorException("lock get wait timeout!key = {$this->key},ttl = {$this->ttl},waitTime = {$this->waitTime}");
            }
            if ($this->_get()) {
                break;
            }
            // 等待10毫秒后重新获取锁
            usleep(10000);
        }
        $this->getTime = time();
        return $this;
    }

    /**
     * @return bool
     * @throws \ErrorException
     */
    private function _get(): bool {
        $redis = $this->getRedis();
        if ($this->lua) {
            $lua = <<<LUA
            local key = KEYS[1]
            local time = ARGV[1]
            local ttl = ARGV[2]
            if redis.call('setnx', key, time) == 1 then
                return redis.call('expire', key, ttl)
            else
                return 0
            end
LUA;
            $lock = $redis->eval($lua, [$this->key, time(), $this->ttl], 1);
            $luaError = $redis->getLastError();
            if (isset($luaError)) {
                throw new \ErrorException($luaError);
            }
        } else {
            $lock = $redis->setnx($this->key, time());
            // 不能获取锁
            if (!$lock) {
                // 判断所是否过期
                $lockTime = $redis->get($this->key);
                if (time() > ($lockTime + $this->ttl)) {
                    // 过期则释放锁，重新获取
                    $this->release();
                    $lock = $redis->setnx($this->key, time());
                    $lock && $redis->expire($this->key, $this->ttl);
                }
            }
        }
        return !!$lock;
    }

    /**
     * 执行超时异常抛出
     * @throws \ErrorException
     */
    public function timeoutThrow(): void {
        if (time() > ($this->getTime + $this->ttl)) {
            // 超时自动释放锁，并抛出异常
            $this->release();
            throw new \ErrorException("handle timeout!key = {$this->key},ttl = {$this->ttl},waitTime = {$this->waitTime}");
        }
    }

    /**
     * 锁释放
     */
    public function release(): void {
        $redis = $this->getRedis();
        $redis->del($this->key);
    }

}